﻿using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;

namespace Hont
{
    public class OCTNode<T>
    {
        public struct WeightInfo
        {
            public float Weight { get; set; }
            public OCTNode<T> Node { get; set; }
        }

        int mTotalItemsCount;
        Bounds mBounds;
        ReadOnlyCollection<IOCTItem<T>> mOCTItemReadonlyCollection;
        ReadOnlyCollection<OCTNode<T>> mChildrenReadonlyCollection;
        List<IOCTItem<T>> mItemsList;
        OCTNode<T>[] mChildrenArr;
        List<WeightInfo> mWeightInfoCacheList;
        IOCTNodeSyncer<T> mSyncer;
        Dictionary<string, object> mUserDataDict;

        bool mHasChildren;

        public Bounds Bounds { get { return mBounds; } }
        public bool HasChildren { get { return mHasChildren; } }
        public int TotalItemsCount { get { return mTotalItemsCount; } }
        public Dictionary<string, object> UserDataDict { get { return mUserDataDict; } }
        public ReadOnlyCollection<IOCTItem<T>> Items { get { return mOCTItemReadonlyCollection; } }
        public ReadOnlyCollection<OCTNode<T>> Childrens { get { return mChildrenReadonlyCollection; } }


        public OCTNode(Vector3 min, Vector3 max, IOCTNodeSyncer<T> syncer)
        {
            mUserDataDict = new Dictionary<string, object>();
            mWeightInfoCacheList = new List<WeightInfo>(8);
            mItemsList = new List<IOCTItem<T>>();
            mChildrenArr = new OCTNode<T>[8];
            mBounds = new Bounds() { min = min, max = max };
            mSyncer = syncer;

            mOCTItemReadonlyCollection = new ReadOnlyCollection<IOCTItem<T>>(mItemsList);
            mChildrenReadonlyCollection = new ReadOnlyCollection<OCTNode<T>>(mChildrenArr);
        }

        public bool AddItem(IOCTItem<T> newItem)
        {
            var result = false;

            if (!mBounds.Contains(newItem.Position)) return result;

            if (!mHasChildren)
            {
                mTotalItemsCount++;
                mItemsList.Add(newItem);
                result = true;
            }
            else
            {
                foreach (var item in mChildrenArr)
                {
                    if (item.AddItem(newItem))
                    {
                        mTotalItemsCount++;
                        result = true;

                        mSyncer.Sync(item);
                    }
                }
            }

            return result;
        }

        public List<IOCTItem<T>> GetItems(Func<OCTNode<T>, float> predicate)
        {
            if (!mHasChildren) return mItemsList;

            mWeightInfoCacheList.Clear();

            foreach (var item in mChildrenArr)
            {
                if (item.TotalItemsCount == 0) continue;

                mWeightInfoCacheList.Add(new WeightInfo() { Weight = predicate(item), Node = item });
            }

            if (mWeightInfoCacheList.Count > 0)
            {
                var compareValue = float.MinValue;
                var resultItem = default(WeightInfo);
                for (int i = 0, iMax = mWeightInfoCacheList.Count; i < iMax; i++)
                {
                    var item = mWeightInfoCacheList[i];

                    if (item.Weight > compareValue)
                    {
                        compareValue = item.Weight;
                        resultItem = item;
                    }
                }

                var maxItem = resultItem;

                if (maxItem.Node == null)
                    return null;
                else
                    return maxItem.Node.GetItems(predicate);
            }
            else
            {
                return null;
            }
        }

        public object GetUserData(string key, object defaultValue)
        {
            object value = null;
            mUserDataDict.TryGetValue(key, out value);

            if (value == null)
                value = defaultValue;

            return value;
        }

        public void Subdivide(int deep)
        {
            var min = mBounds.min;
            var max = mBounds.max;

            var sub = (max - min) * 0.5f;

            var seg1Min = min;
            var seg1Max = min + sub;

            var seg2Min = new Vector3(min.x + sub.x, min.y, min.z);
            var seg2Max = new Vector3(max.x, min.y + sub.y, min.z + sub.z);

            var seg3Min = new Vector3(min.x, min.y + sub.y, min.z);
            var seg3Max = new Vector3(min.x + sub.x, max.y, min.z + sub.z);

            var seg4Min = new Vector3(min.x + sub.x, min.y + sub.y, min.z);
            var seg4Max = new Vector3(max.x, max.y, min.z + sub.z);

            var seg5Min = new Vector3(min.x, min.y, min.z + sub.z);
            var seg5Max = new Vector3(min.x + sub.x, min.y + sub.y, max.z);

            var seg6Min = new Vector3(min.x + sub.x, min.y, min.z + sub.z);
            var seg6Max = new Vector3(max.x, min.y + sub.y, max.z);

            var seg7Min = new Vector3(min.x, min.y + sub.y, min.z + sub.z);
            var seg7Max = new Vector3(min.x + sub.x, max.y, max.z);

            var seg8Min = new Vector3(min.x + sub.x, min.y + sub.y, min.z + sub.z);
            var seg8Max = new Vector3(max.x, max.y, max.z);

            mChildrenArr[0] = new OCTNode<T>(seg1Min, seg1Max, mSyncer);
            mChildrenArr[1] = new OCTNode<T>(seg2Min, seg2Max, mSyncer);
            mChildrenArr[2] = new OCTNode<T>(seg3Min, seg3Max, mSyncer);
            mChildrenArr[3] = new OCTNode<T>(seg4Min, seg4Max, mSyncer);
            mChildrenArr[4] = new OCTNode<T>(seg5Min, seg5Max, mSyncer);
            mChildrenArr[5] = new OCTNode<T>(seg6Min, seg6Max, mSyncer);
            mChildrenArr[6] = new OCTNode<T>(seg7Min, seg7Max, mSyncer);
            mChildrenArr[7] = new OCTNode<T>(seg8Min, seg8Max, mSyncer);

            mHasChildren = true;

            if (deep > 1)
                foreach (var item in Childrens)
                    item.Subdivide(deep - 1);
        }
    }
}
